﻿// $Id: SignalServer.cs 11 2008-08-20 15:24:25Z jkingry $

namespace Mithril.Server
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;

    public class SignalServer
    {
        public const int SignalServerPort = 4512;
        public const int ChannelSize = 17;

        private Socket listener;
        private Channel emptyChannel = new Channel(Guid.Empty, 0);
        private Dictionary<Guid, Channel> map = new Dictionary<Guid, Channel>();
        private ReaderWriterLockSlim mapLock = new ReaderWriterLockSlim();

        private class Channel
        {
            public Channel(Guid id, int messageSize)
            {
                this.Id = id;
                this.MessageSize = messageSize;
            }

            public void Close()
            {
                Trace.TraceInformation("Closing Channel {0}", this.Id);

                this.peersLock.EnterWriteLock();
                foreach (SocketClient client in this.peers)
                {
                    client.Closing = true;
                    client.Socket.Close();
                    client.Channel = null;
                }
                this.peers.Clear();
                this.peersLock.ExitWriteLock();
            }

            public SocketClient[] GetPeers()
            {
                this.peersLock.EnterReadLock();
                try
                {
                    return this.peers.ToArray();
                }
                finally
                {
                    this.peersLock.ExitReadLock();
                }
            }
            public void AddPeer(SocketClient client)
            {
                this.peersLock.EnterWriteLock();
                try
                {
                    this.peers.Add(client);
                    client.Channel = this;
                }
                finally
                {
                    this.peersLock.ExitWriteLock();
                }
            }
            public bool RemovePeer(SocketClient client)
            {
                this.peersLock.EnterWriteLock();
                try
                {
                    this.peers.Remove(client);
                    return this.peers.Count == 0;
                }
                finally
                {
                    this.peersLock.ExitWriteLock();
                }
            }

            private readonly ReaderWriterLockSlim peersLock = new ReaderWriterLockSlim();
            private readonly List<SocketClient> peers = new List<SocketClient>();

            public readonly Guid Id;
            public readonly int MessageSize;
        }

        private class SocketClient
        {
            public readonly Socket Socket;
            public SocketClient(Socket socket)
            {
                this.Socket = socket;
            }

            public bool Closing;
            public Channel Channel;
        }

        public void Start()
        {            
            if (this.listener == null)
            {
                IPEndPoint localEP = new IPEndPoint(IPAddress.Any, SignalServerPort);

                this.listener = new Socket(
                    localEP.Address.AddressFamily,
                    SocketType.Stream,
                    ProtocolType.Tcp);

                this.listener.Bind(localEP);
                this.listener.Listen(10);

                Trace.TraceInformation("Listening on {0}", listener.LocalEndPoint);

                Connect(this.listener);
            }
        }

        public void Stop()
        {
            listener.Close();

            emptyChannel.Close();

            mapLock.EnterWriteLock();
            foreach (Channel channel in map.Values)
            {
                channel.Close();
            }
            map.Clear();
            mapLock.ExitWriteLock();
        }

        private void Connect(Socket listener)
        {
            //TODO: get event args from free list
            SocketAsyncEventArgs e = new SocketAsyncEventArgs();
            e.UserToken = listener;
            //TODO: set AcceptSocket from free list
            //TODO: use SetBuffer to get first command
            e.Completed += new EventHandler<SocketAsyncEventArgs>(Connect_Completed);

            try
            {
                if (!listener.AcceptAsync(e))
                {
                    Trace.TraceInformation("Connect Synchronous");
                    Connect_Completed(this, e);
                }
            }
            catch (ObjectDisposedException)
            {
                return;
            }
        }

        private void Connect_Completed(object sender, SocketAsyncEventArgs e)
        {
            Socket listener = (Socket)e.UserToken;
            if (e.SocketError == SocketError.Success)
            {
                SocketClient client = new SocketClient(e.AcceptSocket);
                emptyChannel.AddPeer(client);

                Trace.TraceInformation("Connect_Completed: {0}", client.Socket.RemoteEndPoint);

                //TODO: what happens if connect completes syncronously?
                Recieve(client, SignalServer.ChannelSize);
            }
            else
            {
                Trace.TraceWarning("Connect_Completed: {0}", e.SocketError);
            }
            Connect(listener);
        }

        private void Recieve(SocketClient client, int messageSize)
        {
            //TODO: get event args from free list
            SocketAsyncEventArgs e = new SocketAsyncEventArgs();
            e.UserToken = client;
            //TODO: reuse central buffer
            e.SetBuffer(new byte[messageSize], 0, messageSize);
            e.Completed += new EventHandler<SocketAsyncEventArgs>(Recieve_Completed);
            if (!client.Socket.ReceiveAsync(e))
            {
                Trace.TraceInformation("Recieve Synchronous");
                Recieve_Completed(this, e);
            }
        }

        void Recieve_Completed(object sender, SocketAsyncEventArgs e)
        {
            SocketClient client = (SocketClient)e.UserToken;
            if (e.SocketError == SocketError.Success)
            {
                int read = e.BytesTransferred;

                Trace.TraceInformation("Receive: {0}, {1} bytes", client.Socket.RemoteEndPoint, read);

                if (read == 0)
                {
                    Close(client);
                    return;
                }

                int totalread = e.Offset + read;
                int expectedsize = client.Channel == this.emptyChannel ? SignalServer.ChannelSize : client.Channel.MessageSize;

                if (totalread != expectedsize)
                {
                    e.SetBuffer(totalread, expectedsize - totalread);
                    if (!client.Socket.ReceiveAsync(e))
                    {
                        Recieve_Completed(this, e);
                    }
                    return;
                }

                if (client.Channel == this.emptyChannel)
                {
                    byte[] cbuf = new byte[16];
                    Buffer.BlockCopy(e.Buffer, 0, cbuf, 0, 16);

                    Guid channel = new Guid(cbuf);
                    RecieveChannel(client, channel, e.Buffer[16]);
                }
                else
                {
                    RecieveSignal(client, e.Buffer);
                }

                Recieve(client, client.Channel.MessageSize);
            }
            else
            {
                Trace.TraceWarning("Recieve_Completed: {0}", e.SocketError);
                Close(client);
            }
        }

        void RecieveChannel(SocketClient client, Guid channelId, int messageSize)
        {
            Trace.TraceInformation("Channel: {0}, channel {1}", client.Socket.RemoteEndPoint, channelId);

            Channel channel = null;

            mapLock.EnterUpgradeableReadLock();
            if (!map.ContainsKey(channelId))
            {
                channel = new Channel(channelId, messageSize);

                mapLock.EnterWriteLock();                                
                try
                {
                    map.Add(channelId, channel);
                }
                finally
                {
                    mapLock.ExitWriteLock();
                    mapLock.ExitUpgradeableReadLock();
                }
            }
            else
            {
                mapLock.EnterReadLock();
                mapLock.ExitUpgradeableReadLock();
                try
                {
                    channel = map[channelId];
                }
                finally
                {
                    mapLock.ExitReadLock();
                }
            }
            emptyChannel.RemovePeer(client);
            channel.AddPeer(client);
        }

        void RecieveSignal(SocketClient client, byte[] message)
        {
            Trace.TraceInformation("Signal: {0}", client.Socket.RemoteEndPoint);

            foreach (SocketClient peer in client.Channel.GetPeers())
            {
                if (peer != client)
                {
                    SendSignal(peer, message);
                }
            }

        }

        void SendSignal(SocketClient client, byte[] message)
        {
            //TODO: get event args from free list
            SocketAsyncEventArgs e = new SocketAsyncEventArgs();
            e.UserToken = client;

            e.SetBuffer(message, 0, client.Channel.MessageSize);

            e.Completed += new EventHandler<SocketAsyncEventArgs>(SendSignal_Completed);

            if (!client.Socket.SendAsync(e))
            {
                Trace.TraceInformation("Send Synchronous");
                SendSignal_Completed(this, e);
            }
        }

        void SendSignal_Completed(object sender, SocketAsyncEventArgs e)
        {
            SocketClient client = (SocketClient)e.UserToken;
            if (e.SocketError == SocketError.Success)
            {
                int sent = e.BytesTransferred;

                Trace.TraceInformation("Signal Sent: {0}, {1} bytes", client.Socket.RemoteEndPoint, sent);
                
                if (sent == 0)
                {
                    Close(client);
                    return;
                }

                int totalsent = e.Offset + sent;

                if (totalsent != client.Channel.MessageSize)
                {
                    e.SetBuffer(totalsent, client.Channel.MessageSize - totalsent);
                    if (!client.Socket.SendAsync(e))
                    {
                        SendSignal_Completed(this, e);
                    }
                    return;
                }
            }
            else
            {
                Trace.TraceWarning("SendSignal_Completed: {0}", e.SocketError);
                Close(client);
            }
        }

        void Close(SocketClient client)
        {
            if (client.Closing) return;
            client.Closing = true;

            Trace.TraceInformation("Close: {0}", client.Socket.RemoteEndPoint);
            Channel channel = client.Channel;

            mapLock.EnterUpgradeableReadLock();
            if (channel.RemovePeer(client))
            {
                mapLock.EnterWriteLock();
                map.Remove(channel.Id);
                mapLock.ExitWriteLock();
            }
            mapLock.ExitUpgradeableReadLock();

            //TODO: get event args from free list
            SocketAsyncEventArgs e = new SocketAsyncEventArgs();
            e.UserToken = client;
            e.DisconnectReuseSocket = true;
            //TODO: use DisconnectReuseSocket
            e.Completed += this.Close_Completed;

            if (!client.Socket.DisconnectAsync(e))
            {
                Trace.TraceInformation("Close Synchronous");
                Close_Completed(this, e);
            }
        }

        void Close_Completed(object sender, SocketAsyncEventArgs e)
        {
            SocketClient client = (SocketClient)e.UserToken;
            if (e.SocketError == SocketError.Success)
            {
                Trace.TraceInformation("Close Completed: {0}", ((SocketClient)e.UserToken).Socket.RemoteEndPoint);
                //TODO: add socket back to free list
            }
            else
            {
                Trace.TraceWarning("Close_Completed: {0}", e.SocketError);
            }
        }
    }
}
